# coding:utf-8
'''
时间序列数据的意义取决于具体的应⽤场景，主要有以下⼏种：
    @时间戳（timestamp），特定的时刻。
    @固定时期（period），如2007年1⽉或2010年全年。
    @时间间隔（interval），由起始和结束时间戳表示。时期（period）可以被看做间隔（interval）的特例。
    @实验或过程时间，每个时间点都是相对于特定起始时间的⼀个度量。例如，从放⼊烤箱时起，每秒钟饼⼲的直径。
'''
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from datetime import datetime

# 柯里化一个print
printn = lambda *x: print(*x, sep='\n')

# ⽇期和时间数据类型及⼯具,主要有datetime,time,calendar模块
'''
date-存储日期(年、月、日)；time--存储时间(时、分、秒、毫秒)；datetime--存储(日期和时间)；timedelta--存储差值(日、秒、毫秒)
'''


def timetools():
    now = datetime.now()
    printn("现在的时间:", now)
    print("当前年,月,日:", now.year, now.month, now.day)
    # 上面的now是一个无时区的类型，用tzinfo转换成有时区的
    import pytz
    # printn("filter过滤出亚洲时区名称:",list(filter(lambda x:str(x).startswith('Asia'),list(pytz.common_timezones))))
    now2 = now.replace(tzinfo=pytz.timezone('Asia/Shanghai'))
    printn("有时区时间为:", now2)

    # datetime以毫秒形式存储⽇期和时间。timedelta表示两个datetime对象之间的时间差
    delta = datetime(2011, 1, 7) - datetime(2008, 6, 24, 8, 15)
    printn("时间差:", delta)
    print("时间差的天和秒:", delta.days, delta.seconds)

    # 给datetime对象加上（或减去）⼀个或多个timedelta，这样会产⽣⼀个新对象
    from datetime import timedelta
    start = datetime(2011, 1, 7)
    time1 = start + timedelta(12)  # 12天
    time2 = start - 2 * timedelta(12)
    printn("初始时间:", start, "time1:", time1, "time2:", time2)
# timetools()


# 字符串和datetime的相互转换
'''
%Y-4位数字年;%y-2位数字年;%m-2位数字月;%d-2位数字日;
%H--24小时制时;%h--12小时制时;%M--2位数字分;%S-秒[00,61]
%w--整数表示星期几(0~6);
%U--每年的第几周(00~53),周日认为第一天，第一个周日前为第0周
%W--每年的第几周(00~53),周一认为第一天，第一个周一前为第0周
%z--以+HHMM或-HHMM表示UTC时区偏移量
%F--%Y-%m-%d的简写形式
%D--%m/%d/%y的简写形式
--------------
不同国家可能有不同解释的格式：
%a--星期几的简写;%A--星期几的全称;%b--月份的简写;%B--月份的全称
%c--完整的日期和时间，如"Tue 01 May 2012 04:50:57 PM"
%p--不同环境中的AM或PM
'''


def strtrans():
    # 利⽤str或strftime⽅法（传⼊⼀个格式化字符串），datetime对象和pandas的Timestamp对象（稍后就会介绍）可以被格式化为字符串
    stamp = datetime(2011, 1, 3)
    printn(
        "str函数转字符串:", str(stamp), "strftime方法转字符串:",
        stamp.strftime('%Y-%m-%d'))

    # datetime.strptime可以⽤这些格式化编码将字符串转换为⽇期
    value = '2011-01-03'
    printn('字符串转时间1:', datetime.strptime(value, '%Y-%m-%d'))
    datestrs = ['7/6/2011', '8/6/2011']
    printn('字符串转时间2:', [datetime.strptime(x, '%m/%d/%Y') for x in datestrs])
    '''
    datetime.strptime是通过已知格式进⾏⽇期解析的最佳⽅式。但是每次都要编写格式定义是很麻烦的事情，尤其是对于⼀些常⻅的⽇期格式。
    这种情况下，你可以⽤dateutil这个第三⽅包中的parser.parse⽅法（pandas中已经⾃动安装好了）
    '''
    from dateutil.parser import parse
    printn('parse解析字符串:', parse(value))
    # dateutil可以解析⼏乎所有⼈类能够理解的⽇期表示形式
    printn('parse解析字符串2:', parse('Jan 31, 1997 10:45 PM'))
    # 在国际通⽤的格式中，⽇出现在⽉的前⾯很普遍，传⼊dayfirst=True即可解决这个问题
    printn('parse解析字符串-日在月前:', parse('6/12/2011', dayfirst=True))

    # pandas通常是⽤于处理成组⽇期的，不管这些⽇期是DataFrame的轴索引还是列
    # to_datetime⽅法可以解析多种不同的⽇期表示形式。对标准⽇期格式（如ISO8601）的解析⾮常快
    datestrs = ['2011-07-06 12:00:00', '2011-08-06 00:00:00']
    printn("一组list日期转index:", pd.to_datetime(datestrs))
    # 它还可以处理缺失值（None、空字符串等):
    idx = pd.to_datetime(datestrs + [None])
    printn("一组list日期带None值，转index:", pd.to_datetime(idx))
    printn("NaT表示时间戳数据的null值:", idx[2], '是否null:', pd.isnull(idx))
# strtrans()


# 时间序列基础
# pandas最基本的时间序列类型就是以时间戳（通常以Python字符串或datatime对象表示）为索引的Series
def timelist():
    dates = [
        datetime(2011, 1, 2), datetime(2011, 1, 5), datetime(2011, 1, 7),
        datetime(2011, 1, 8), datetime(2011, 1, 10), datetime(2011, 1, 12)
    ]
    ts = pd.Series(np.random.randn(6), index=dates)
    printn("时间索引例子:", ts)

    # 这些datetime对象实际上是被放在⼀个DatetimeIndex中的，⽤NumPy的datetime64数据类型以纳秒形式存储时间戳
    printn("时间索引:", ts.index, '时间索引类型:', ts.index.dtype)
    # 不同索引的时间序列之间的算术运算会⾃动按⽇期对⻬
    printn("时间序列计算:", ts + ts[::2])
    # DatetimeIndex中的各个标量值是pandas的Timestamp对象
    printn("时间序列的标量值:", [ts.index[0], ])
# timelist()


# 索引、选取、⼦集构造;切片获取的是视图，不是副本
def timeselect():
    dates = [
        datetime(2011, 1, 2), datetime(2011, 1, 5), datetime(2011, 1, 7),
        datetime(2011, 1, 8), datetime(2011, 1, 10), datetime(2011, 1, 12)
    ]
    ts = pd.Series(np.random.randn(6), index=dates)
    printn("时间索引例子:", ts)

    # 根据标签索引选取数据时，时间序列和其它的pandas.Series很像
    stamp = ts.index[2]
    printn('时间序列索引:', ts[stamp])
    # 还有⼀种更为⽅便的⽤法：传⼊⼀个可以被解释为⽇期的字符串
    printn('直接传入日期字符串:', ts['1/10/2011'], ts['20110110'])

    # 对于较⻓的时间序列，只需传⼊“年”或“年⽉”即可轻松选取数据的切⽚
    longer_ts = pd.Series(
        np.random.randn(1000),
        index=pd.date_range('1/1/2000', periods=1000))
    printn("切出某一年的数据:", longer_ts['2001'])
    printn('切出某年某月的数据:', longer_ts['2001-05'])

    # datetime对象也可以进行切片
    printn("某个datetime及之后的数据:", ts[datetime(2011, 1, 7):])

    # 由于⼤部分时间序列数据都是按照时间先后排序的，因此你也可以⽤不存在于该时间序列中的时间戳对其进⾏切⽚
    # （即范围查询），其实也可以传入字符串⽇期、datetime或Timestamp
    printn("不存在序列的时间来切片:", ts['1/6/2011':'1/11/2011'])

    # 还有⼀个等价的实例⽅法也可以截取两个⽇期之间TimeSeries
    printn("truncate函数使用:", ts.truncate(after='1/8/2011'))

    # 这些操作对DataFrame也有效。例如，对DataFrame的⾏进⾏索引
    dates = pd.date_range('1/1/2000', periods=100, freq='W-WED')
    long_df = pd.DataFrame(
        np.random.randn(100, 4),
        index=dates,
        columns=['Colorado', 'Texas', 'New York', 'Ohio'])
    printn("某年某月切片:", long_df['5-2001'])
# timeselect()


# 带有重复索引的时间序列
# 在某些应⽤场景中，可能会存在多个观测数据落在同⼀个时间点上的情况
def timedup():
    dates = pd.DatetimeIndex(
        ['1/1/2000', '1/2/2000', '1/2/2000', '1/2/2000', '1/3/2000'])
    dup_ts = pd.Series(np.arange(5), index=dates)
    printn("设定的ts序列:", dup_ts)

    # 通过检查索引的is_unique属性，我们就可以知道它是不是唯⼀的
    printn("唯一性检查:", dup_ts.index.is_unique)
    # 对这个时间序列进⾏索引，要么产⽣标量值，要么产⽣切⽚，具体要看所选的时间点是否重复
    printn("索引的结果可能是标量，也可能是切片:", dup_ts['1/3/2000'], dup_ts['1/2/2000'])

    # 对具有⾮唯⼀时间戳的数据进⾏聚合。⼀个办法是使⽤groupby，并传⼊level=0
    grouped = dup_ts.groupby(level=0)
    printn("group后mean:", grouped.mean(), "group后count:", grouped.count())
# timedup()


# ⽇期的范围、频率以及移动
'''
pandas中的原⽣时间序列⼀般被认为是不规则的，也就是说，它们没有固定的频率。对于⼤部分应⽤程序⽽⾔，这是⽆所谓的。
但是，它常常需要以某种相对固定的频率进⾏分析，⽐如每⽇、每⽉、每15分钟等（这样⾃然会在时间序列中引⼊缺失值）。
幸运的是，pandas有⼀整套标准时间序列频率以及⽤于重采样、频率推断、⽣成固定频率⽇期范围的⼯具。
例如，我们可以将之前那个时间序列转换为⼀个具有固定频率（每⽇）的时间序列，只需调⽤resample即可
'''


# 生成日期范围
def periodrange():
    # 默认情况下，date_range会产⽣按天计算的时间点
    index = pd.date_range('2012-04-01', '2012-06-01')
    printn("设定的index:", index)
    # 如果只传⼊起始或结束⽇期，那就还得传⼊⼀个表示⼀段时间的数字
    index2 = pd.date_range(start='2012-04-01', periods=20)
    printn("只设定起始的index:", index2)
    index3 = pd.date_range(end='2012-06-01', periods=20)
    printn("只设定结束的index:", index3)

    # ⽣成⼀个由每⽉最后⼀个⼯作⽇组成的⽇期索引，可以传⼊"BM"频率（表示business end of month）
    index4 = pd.date_range('2000-01-01', '2000-12-01', freq='BM')
    printn("设定频率BM的index:", index4)
    '''
    基本频率表：
    D-每日历日；B-每工作日；H-每小时；T/min-每分；S-每秒；L/ms-每毫秒；U-每微秒；M-每月最后一个日历日
    BM-每月最后一个工作日；MS-每月第一个日历日；BMS-每月第一个工作日；W-MON，W-TUE等--每周从指定的星期几开始算起；
    WOM-1MON，WOM-2MON等--产生每月第一、二...周的星期几，例如WOM-3FRI表示每月第3个星期五
    Q-JAN、Q-FEB等--对于以指定月份结束的年度，每季度最后一月的最后一个日历日；
    BQ-JAN、BQ-FEB等--对于以指定月份结束的年度，每季度最有一月的最后一个工作日；
    QS-JAN、QS-FEB等--对于以指定月份结束的年度，每季度最后一月的第一个日历日；
    BQS-JAN、BQS-FEB等--对于以指定月份结束的年度，每季度最后一月的第一个工作日；
    A-JAN、A-FEB等--每年指定月份的最后一个日历日；
    BA-JAN、BA-FEB等--每年指定月份的最后一个工作日；
    AS-JAN、AS-FEB等--每年指定月份的第一个日历日；
    BAS-JAN、BAS-FEB等--每年指定月份的第一个工作日；
    '''

    # date_range默认会保留起始和结束时间戳的时间信息（如果有的话）
    index5 = pd.date_range('2012-05-02 12:56:31', periods=5)
    printn("保留起始时间戳:", index5)
    # 虽然起始和结束⽇期带有时间信息，但你希望产⽣⼀组被规范化（normalize）到午夜的时间戳
    index6 = pd.date_range('2012-05-02 12:56:31', periods=5, normalize=True)
    printn("规范时间戳到0点，日期后面的时间不显示:", index6)
# periodrange()


# 频率和日期偏移量
def feqoffset():
    '''
    pandas中的频率是由⼀个基础频率（base frequency）和⼀个乘数组成的。基础频率通常以⼀个字符串别名表示，⽐如"M"表示每⽉，"H"表示每⼩时。对于每个基础频率，都有⼀个被称为⽇期偏移量（date offset）的对象与之对应
    '''
    from pandas.tseries.offsets import Hour, Minute
    hour = Hour()
    printn('Hour实例:', hour)
    # 传⼊⼀个整数即可定义偏移量的倍数
    four_hours = Hour(4)
    printn('4小时偏移:', four_hours)

    # ⼀般来说，⽆需明确创建这样的对象，只需使⽤诸如"H"或"4H"这样的字符串别名即可。在基础频率前⾯放上⼀个整数即可创建倍数
    index = pd.date_range('2000-01-01', '2000-01-03 23:59', freq='4h')
    printn('每4小时的时间:', index)

    # 大部分偏移量对象都可通过加法进⾏连接
    printn("偏移量加法:", Hour(2) + Minute(30))
    # 你也可以传⼊频率字符串（如"2h30min"），这种字符串可以被⾼效地解析为等效的表达式
    # 有些频率所描述的时间点并不是均匀分隔的,将这些称为锚点偏移量（anchored offset）,如“M”日历月末
    index2 = pd.date_range('2000-01-01', periods=10, freq='1h30min')
    printn('每1个半小时的时间(10个):', index2)

    # WOM（Week Of Month）是⼀种⾮常实⽤的频率类，它以WOM开头。它使你能获得诸如“每⽉第3个星期五”之类的⽇期
    rng = pd.date_range('2012-01-01', '2012-09-01', freq='WOM-3FRI')
    printn('WOM-3FRI设定的日期:', list(rng))
# feqoffset()


# 移动（超前和滞后）数据
def shiftdata():
    # 移动（shifting）指的是沿着时间轴将数据前移或后移。Series和DataFrame都有⼀个shift⽅法⽤于执⾏单纯的前移或后移操作，保持索引不变
    ts = pd.Series(
        np.random.randn(4),
        index=pd.date_range('1/1/2000', periods=4,
                            freq='M'))
    printn('原始序列:', ts)

    # 前移、后移2位
    printn('前移2位:', ts.shift(2), '后移2位:', ts.shift(-2))
    # 当我们这样进⾏移动时，就会在时间序列的前⾯或后⾯产⽣缺失数据
    # shift通常⽤于计算⼀个时间序列或多个时间序列（如DataFrame的列）中的百分⽐变化。可以这样表达：
    printn('百分比变化:', ts / ts.shift(1) - 1)

    # 由于单独的移位操作不会修改索引，所以部分数据会被丢弃
    # 如果频率已知，则可以将其传给shift以便实现对时间戳进⾏位移⽽不是对数据进⾏简单位移
    printn('对时间戳位移(一月):', ts.shift(2, freq='M'))
    # 也可以使用其他频率
    printn(
        '对时间戳位移2(一天):',
        ts.shift(2, freq='D'),
        '对时间戳位移3(一个半小时):',
        ts.shift(1, freq='90T'))
# shiftdata()


# 通过偏移量对⽇期进⾏位移
def shiftbyoffset():
    # pandas的⽇期偏移量还可以⽤在datetime或Timestamp对象上
    from pandas.tseries.offsets import Day, MonthEnd
    now = datetime(2011, 11, 17)
    printn('前移3天:', [now + 3 * Day(), ])

    # 如果加的是锚点偏移量(如MonthEnd)，第⼀次增量会将原⽇期向前滚动到符合频率规则的下⼀个⽇期
    printn('锚点偏移量:', [now + MonthEnd(), ], '锚点偏移量2:', [now + MonthEnd(2), ])
    # 通过锚点偏移量的rollforward和rollback⽅法，可明确地将⽇期向前或向后“滚动”
    offset = MonthEnd()
    printn(
        'rollforword向前:', offset.rollforward(now), 'rollback向后:',
        offset.rollback(now))

    # ⽇期偏移量还有⼀个巧妙的⽤法，即结合groupby使⽤这两个“滚动”⽅法
    ts = pd.Series(
        np.random.randn(20),
        index=pd.date_range('1/15/2000', periods=20,
                            freq='4d'))
    printn("设定的序列：", ts)
    printn('groupby调用rollforward方法:', ts.groupby(offset.rollforward).mean())
    # 更简单、更快速地实现该功能的办法是使⽤resample
    printn('resample实现:', ts.resample('M').mean())
# shiftbyoffset()


# 时区处理
def testtzone():
    import pytz
    # 时区名称
    printn('后5个时区的名称:', pytz.common_timezones[-5:])
    # 要从pytz中获取时区对象，使⽤pytz.timezone即可
    tz = pytz.timezone('America/New_York')
    printn('时区对象:', [tz, ])

    # 时区本地化与转换
    # 默认情况下，pandas中的时间序列是单纯（naive）的时区。看看下⾯这个时间序列
    rng = pd.date_range('3/9/2012 9:30', periods=6, freq='D')
    ts = pd.Series(np.random.randn(len(rng)), index=rng)
    printn("设定的序列：", ts, '索引的tz字段:', ts.index.tz)

    # 可以⽤时区集⽣成⽇期范围
    rng2 = pd.date_range('3/9/2012 9:30', periods=10, freq='D', tz='UTC')
    printn('时区集生成日期范围:', rng2)
    # 从单纯到本地化的转换是通过tz_localize⽅法处理的
    ts_utc = ts.tz_localize('UTC')
    printn('ts从单纯到本地化:', ts_utc, '本地化后索引:', ts_utc.index)
    # ⼀旦时间序列被本地化到某个特定时区，就可以⽤tz_convert将其转换到别的时区了
    ts_conv = ts_utc.tz_convert('America/New_York')
    printn('ts_utc转换时区:', ts_conv)

    # 对于上⾯这种时间序列（它跨越了美国东部时区的夏令时转变期），我们可以将其本地化到EST，然后转换为UTC或柏林时间
    ts_eastern = ts.tz_localize('America/New_York')
    printn(
        'ts_eastern转换到UTC:', ts_eastern.tz_convert('UTC'),
        'ts_eastern转换到Berlin:', ts_eastern.tz_convert('Europe/Berlin'))

    # tz_localize和tz_convert也是DatetimeIndex的实例⽅法
    printn('DatetimeIndex实例方法:', ts.index.tz_localize('Asia/Shanghai'))

    # 操作时区意识型Timestamp对象
    # 跟时间序列和⽇期范围差不多，独⽴的Timestamp对象也能被从单纯型（naive）本地化为时区意识型（time zone-aware），并从⼀个时区转换到另⼀个时区
    stamp = pd.Timestamp('2011-03-12 04:00')
    stamp_utc = stamp.tz_localize('utc')
    stamp_conv = stamp_utc.tz_convert('America/New_York')
    printn('naive转为时区意识型:', [stamp_conv, ])

    # 在创建Timestamp时，还可以传⼊⼀个时区信息
    stamp_moscow = pd.Timestamp('2011-03-12 04:00', tz='Europe/Moscow')
    printn('传入时区信息给timestamp:', [stamp_moscow, ])
    # 时区意识型Timestamp对象在内部保存了⼀个UTC时间戳值（⾃UNIX纪元（1970年1⽉1⽇）算起的纳秒数）。
    # 这个UTC值在时区转换过程中是不会发⽣变化的
    printn("直接value:", stamp_utc.value, "转换后的value:", stamp_conv.value)

    # DateOffset对象执⾏时间算术运算时，运算过程会⾃动关注是否存在夏令时转变期
    from pandas.tseries.offsets import Hour
    stamp = pd.Timestamp('2012-03-12 01:30', tz='US/Eastern')
    stamp1 = stamp + Hour()
    printn("初始时间：", stamp, "1小时后:", stamp1)
    # 夏令时转变前90分钟:
    stamp = pd.Timestamp('2012-11-04 00:30', tz='US/Eastern')
    stamp2 = stamp + 2 * Hour()
    printn("初始时间2：", stamp, "2小时后:", stamp2)

    # 不同时区之间的运算
    # 两个时间序列的时区不同，在将它们合并到⼀起时，最终结果就会是UTC.
    # 由于时间戳其实是以UTC存储的，所以这是⼀个很简单的运算，并不需要发⽣任何转换
    rng = pd.date_range('3/7/2012 9:30', periods=10, freq='B')
    ts = pd.Series(np.random.rand(len(rng)), index=rng)
    printn("初始序列:", ts)
    ts1 = ts[:7].tz_localize('Europe/London')
    ts2 = ts1[2:].tz_convert('Europe/Moscow')
    result = ts1 + ts2
    printn("相加后结果的index:", result.index)
# testtzone()


# 时期及其算术运算
# 时期（period）表示的是时间区间，⽐如数⽇、数⽉、数季、数年等。
def testperiods():
    # Period类所表示的就是这种数据类型，其构造函数需要⽤到⼀个字符串或整数，以及频率
    p = pd.Period(2007, freq='A-DEC')
    printn("初始时期：", [p, ])
    # 这个Period对象表示的是从2007年1⽉1⽇到2007年12⽉31⽇之间的整段时间。
    # 只需对Period对象加上或减去⼀个整数即可达到根据其频率进⾏位移的效果
    printn("前移5年:", [p + 5, ], "后移两年:", [p - 2, ])
    # 如果两个periods对象拥有相同的频率，则它们的差就是它们之间的单位数量
    pdelta = pd.Period('2014', freq='A-DEC') - p
    printn("periods差:", pdelta)

    # period_range函数可⽤于创建规则的时期范围,返回一个PeriodIndex类
    rng = pd.period_range('2000-01-01', '2000-06-30', freq='M')
    printn('period_range函数:', rng)
    # PeriodIndex类保存了⼀组Period，它可以在任何pandas数据结构中被⽤作轴索引
    s = pd.Series(np.random.randn(6), index=rng)
    printn('PeriodIndex类索引:', s)
    # 如果你有⼀个字符串数组，你也可以使⽤PeriodIndex类
    values = ['2001Q3', '2002Q2', '2003Q1']
    index = pd.PeriodIndex(values, freq='Q-DEC')
    printn('字符串数组使用PeriodIndex类:', index)
# testperiods()


# 时期的频率转换
def Period_conv():
    # Period和PeriodIndex对象都可以通过其asfreq⽅法被转换成别的频率
    p = pd.Period('2007', freq='A-DEC')
    p1 = p.asfreq('M', how='start')
    p2 = p.asfreq('M', how='end')
    # 将Period('2007','A-DEC')看做⼀个被划分为多个⽉度时期的时间段中的游标
    printn("初始时期:", [p, ], '使用start转换:', [p1, ], '使用end转换:', [p2, ])

    # 对于⼀个不以12⽉结束的财政年度，⽉度⼦时期的归属情况就不⼀样
    p = pd.Period('2007', freq='A-JUN')
    p1 = p.asfreq('M', how='start')
    p2 = p.asfreq('M', how='end')
    printn("初始时期2:", [p, ], '使用start转换2:', [p1, ], '使用end转换2:', [p2, ])

    # 在将⾼频率转换为低频率时，超时期（superperiod）是由⼦时期（subperiod）所属的位置决定的
    p = pd.Period('Aug-2007', 'M')
    # A-JUN是6月的最后一天,索引到该日期，则到了下一年
    printn('初始时期3:', [p, ], '转位低频率:', [p.asfreq('A-JUN'), ])

    # 完整的PeriodIndex或TimeSeries的频率转换⽅式也是如此：
    rng = pd.period_range('2006', '2009', freq='A-DEC')
    ts = pd.Series(np.random.randn(len(rng)), index=rng)
    # 根据年度时期的第⼀个⽉，每年的时期被取代为每⽉的时期
    ts1 = ts.asfreq('M', how='start')
    printn("初始序列:", [ts, ], '使用start转换:', [ts1, ])
    # 想要每年的最后⼀个⼯作⽇，我们可以使⽤“B”频率，并指明想要该时期的末尾
    ts2 = ts.asfreq('B', how='end')
    printn('使用end转换:', ts2)
# Period_conv()


# 按季度计算的时期频率
'''
季度型数据在会计、⾦融等领域中很常⻅。许多季度型数据都会涉及“财年末”的概念，
通常是⼀年12个⽉中某⽉的最后⼀个⽇历⽇或⼯作⽇。
就这⼀点来说，时期"2012Q4"根据财年末的不同会有不同的含义。
pandas⽀持12种可能的季度型频率，即Q-JAN到Q-DEC
'''


def Qcal():
    # Q-xxx表示xxx为最有一季度的最后一月
    p = pd.Period('2012Q4', freq='Q-JAN')
    printn('第4季度:', [p, ])
    # 在以1⽉结束的财年中，2012Q4是从11⽉到1⽉（将其转换为⽇型频率就明⽩了）
    p1 = p.asfreq('D', 'start')
    p2 = p.asfreq('D', 'end')
    printn('使用start转换:', [p1, ], '使用end转换:', [p2, ])

    # Period之间的算术运算会⾮常简单。例如，要获取该季度倒数第⼆个⼯作⽇下午4点的时间戳
    p4pm = (p.asfreq('B', 'e') - 1).asfreq('T', 's') + 16 * 60
    printn('Period表示:', [p4pm, ], 'Timestamp表示:', [p4pm.to_timestamp(), ])

    # period_range可⽤于⽣成季度型范围。季度型范围的算术运算也跟上⾯是⼀样的
    rng = pd.period_range('2011Q3', '2012Q4', freq='Q-JAN')
    ts = pd.Series(np.arange(len(rng)), index=rng)
    printn('原始的ts:', ts)
    new_rng = (rng.asfreq('B', 'e') - 1).asfreq('T', 's') + 16 * 60
    ts.index = new_rng.to_timestamp()
    printn('新的ts:', ts)
# Qcal()


# 将Timestamp转换为Period（及其反向过程）
def Periodtrans():
    # 通过使⽤to_period⽅法，可以将由时间戳索引的Series和DataFrame对象转换为以时期索引
    rng = pd.date_range('2000-01-01', periods=3, freq='M')
    ts = pd.Series(np.random.randn(3), index=rng)
    pts = ts.to_period()
    printn('设定的日子:', rng, '设定日期的Series:', ts, '转换为时期索引:', pts)
    # 由于时期指的是⾮重叠时间区间，因此对于给定的频率，⼀个时间戳只能属于⼀个时期
    # 新PeriodIndex的频率默认是从时间戳推断⽽来的，你也可以指定任何别的频率。结果中允许存在重复时期
    rng = pd.date_range('1/29/2000', periods=6, freq='D')
    ts2 = pd.Series(np.random.randn(6), index=rng)
    pts2 = ts2.to_period('M')
    printn('设定日期的Series2:', ts2, '转换为时期索引2:', pts2)
    # 要转换回时间戳，使用to_timestamp即可：
    pts = ts2.to_period()
    ptstrans = pts.to_timestamp(how='end')
    printn('转换为时期索引:', pts, '转回时间戳:', ptstrans)
# Periodtrans()


# 通过数组创建PeriodIndex
def PeriodIndexCreate():
    # 固定频率的数据集通常会将时间信息分开存放在多个列中。
    # 例如，在下⾯这个宏观经济数据集中，年度和季度就分别存放在不同的列中
    data = pd.read_csv('examples/macrodata.csv')
    printn("读取数据的前5行:", data.head())
    # 通过将这些数组以及⼀个频率传⼊PeriodIndex，就可以将它们合并成DataFrame的⼀个索引
    index = pd.PeriodIndex(year=data.year, quarter=data.quarter, freq='Q-DEC')
    printn("创建的PeriodIndex:", index)
    data.index = index
    printn("data的信息:", data.infl)
# PeriodIndexCreate()


# 重采样及频率转换
'''
重采样（resampling）指的是将时间序列从⼀个频率转换到另⼀个频率的处理过程。
将⾼频率数据聚合到低频率称为降采样（downsampling），⽽将低频率数据转换到⾼频率则称为升采样（upsampling）。
并不是所有的重采样都能被划分到这两个⼤类中。例如，将W-WED（每周三）转换为W-FRI既不是降采样也不是升采样
'''


def dataresample():
    # pandas对象都带有⼀个resample⽅法，它是各种频率转换⼯作的主⼒函数。
    '''
    resample方法参数：
    freq--表示重采样频率的字符串或DataOffset，如"M",'5min'或Second(15)
    axis--重采样的轴，默认axis=0；
    fill_method--升采样如何插值，比如ffill或bfill.默认不插值
    closed--在降采样中，个时间段的哪一段是闭合(即包含)的，right或left，默认right
    label--在降采样中，如何设置聚合值的标签，right或left(面元的右边界或左边界)。如9:30到9：35之间的5分钟会被标记为9：30或9:35；
    loffset--面元标签的时间校正值，比如'-1s'/Second(-1)用于将聚合标签调早1秒
    limit--在前向或后向填充时，允许填充的最大时期数
    kind--聚合到周期('period')或时间戳('timestamp'),默认聚合到时间序列的索引类型
    convention--当对周期进行重采样，将低频周期转换为高频的惯用法('start'或'end');默认'end'
    '''

    # resample有⼀个类似于groupby的API，调⽤resample可以分组数据，然后会调⽤⼀个聚合函数：
    rng = pd.date_range('2000-01-01', periods=100, freq='D')
    ts = pd.Series(np.random.randn(len(rng)), index=rng)
    rts = ts.resample('M').mean()
    rts2 = ts.resample('M', kind='period').mean()
    printn("初始数据:", ts, '按M重采样后:', rts, '聚合到周期(period):', rts2)

    # 降采样
    # 待聚合的数据不必拥有固定的频率，期望的频率会⾃动定义聚合的⾯元边界，这些⾯元⽤于将时间序列拆分为多个⽚段
    # 在⽤resample对数据进⾏降采样时，需要考虑两样东⻄：1、各区间哪边是闭合的。2、如何标记各个聚合⾯元，⽤区间的开头还是末尾。
    rng = pd.date_range('2000-01-01', periods=12, freq='T')
    ts = pd.Series(np.arange(12), index=rng)
    # 假设你想要通过求和的⽅式将这些数据聚合到“5分钟”块中
    # 传⼊的频率将会以“5分钟”的增量定义⾯元边界。默认情况下，⾯元的右边界是包含的，因此00:00到00:05的区间中是包含00:05的
    rts = ts.resample('5min', closed='right').sum()
    rts2 = ts.resample('5min',
                       closed='left').sum()  # 传⼊closed='left'会让区间以左边界闭合
    printn('原始数据:', ts, '5min聚合后求和:', rts, '5min聚合后求和2:', rts2)
    # 最终的时间序列是以各⾯元右边界的时间戳进⾏标记的。传⼊label='right'即可⽤⾯元的右边界对其进⾏标记
    rts3 = ts.resample('5min', closed='right', label='right').sum()
    printn('5min聚合后求和(右边界标记):', rts3)

    # 希望对结果索引做⼀些位移，⽐如从右边界减去⼀秒以便更容易明⽩该时间戳到底表示的是哪个区间。
    # 只需通过loffset设置⼀个字符串或⽇期偏移量即可实现这个⽬的
    rts4 = ts.resample('5min', closed='right', loffset='-1s').sum()
    rts5 = ts.resample(
        '5min', closed='right',
        label='right', loffset='-1s').sum()
    printn('设置loffset位置后1:', rts4, '设置loffset位置后2:', rts5)
    # 也可以通过调⽤结果对象的shift⽅法来实现该⽬的，这样就不需要设置loffset了

    # OHLC重采样
    '''
    ⾦融领域中有⼀种⽆所不在的时间序列聚合⽅式，即计算各⾯元的四个值：第⼀个值（open，开盘）、最后⼀个值（close，收盘）、最⼤值（high，最⾼）以及最⼩值（low，最低）。传⼊how='ohlc'即可得到⼀个含有这四种聚合值的DataFrame。整个过程很⾼效，只需⼀次扫描即可计算出结果
    '''
    ohlcresample = ts.resample('5min').ohlc()
    print('OHLC重采样:', ohlcresample)

    # 升采样和插值
    frame = pd.DataFrame(
        np.random.randn(2, 4),
        index=pd.date_range('1/1/2000', periods=2,
                            freq='W-WED'),
        columns=['Colorado', 'Texas', 'NewYork', 'Ohio'])
    printn('设定的DF:', frame)
    # 使⽤asfreq⽅法转换成⾼频，不经过聚合
    df_daily = frame.resample('D').asfreq()
    printn('asfreq后填NA:', df_daily)
    # ⽤前⾯的周型值填充“⾮星期三”。resampling的填充和插值⽅式跟fillna和reindex的⼀样
    rts6 = frame.resample('D').ffill()
    # 这⾥也可以只填充指定的时期数（⽬的是限制前⾯的观测值的持续使⽤距离）
    rts7 = frame.resample('D').ffill(limit=2)
    printn('asfreq前向填充:', rts6, '只前填充2个:', rts7)
    # 新的日期索引完全没必要跟旧的重叠
    rts8 = frame.resample('W-THU').ffill()
    printn('asfreq前向填充(其他索引):', rts8)

    # 通过时期进行重采样
    # 对那些使⽤时期索引的数据进⾏重采样与时间戳很像
    frame = pd.DataFrame(
        np.random.randn(24, 4),
        index=pd.period_range('1-2000', '12-2001',
                              freq='M'),
        columns=['Colorado', 'Texas', 'NewYork', 'Ohio'])
    printn('设定数据前5行:', frame.head())
    annual_frame = frame.resample('A-DEC').mean()
    printn('年度数据:', annual_frame)
    # 升采样要稍微麻烦⼀些，因为你必须决定在新频率中各区间的哪端⽤于放置原来的值，就像asfreq⽅法那样。
    # convention参数默认为'start'，也可设置为'end'
    rts9 = annual_frame.resample('Q-DEC').ffill()
    rts10 = annual_frame.resample('Q-DEC', convention='end').ffill()
    printn('升采样开始端放置原值:', rts9, '升采样结束端放置原值:', rts10)
    '''
    时期指的是时间区间，所以升采样和降采样的规则就⽐较严格
    1、在降采样中，⽬标频率必须是源频率的⼦时期（subperiod)
    2、在升采样中，⽬标频率必须是源频率的超时期（superperiod）
    不满⾜这些条件，就会引发异常。这主要影响的是按季、年、周计算的频率
    如：由Q-MAR定义的时间区间只能升采样为A-MAR、A-JUN、A-SEP、A-DEC等
    '''
    rts11 = annual_frame.resample('Q-MAR').ffill()
    printn('超时期升采样:', rts11)
# dataresample()


# 移动窗口函数
'''
在移动窗⼝（可以带有指数衰减权数）上计算的各种统计函数也是⼀类常⻅于时间序列的数组变换。
这样可以圆滑噪⾳数据或断裂数据。我将它们称为移动窗⼝函数（moving window function），
其中还包括那些窗⼝不定⻓的函数（如指数加权移动平均）。
跟其他统计函数⼀样，移动窗⼝函数也会⾃动排除缺失值。
'''


def windowfunc():
    close_px_all = pd.read_csv(
        'examples/stock_px_2.csv',
        parse_dates=True,
        index_col=0)
    close_px = close_px_all[['AAPL', 'MSFT', 'XOM']]
    # 将其重采样为⼯作⽇频率
    close_px = close_px.resample('B').ffill()
    # 现在引⼊rolling运算符，它与resample和groupby很像。
    # 可以在TimeSeries或DataFrame以及⼀个window（表示期数）上调⽤它
    # 表达式rolling(250)与groupby很像，但不是对其进⾏分组，⽽是创建⼀个按照250天分组的滑动窗⼝对象。
    # 然后，我们就得到了苹果公司股价的250天的移动窗⼝。
    close_px.AAPL.plot()
    close_px.AAPL.rolling(250).mean().plot()
    # plt.show()

    # 默认情况下，rolling函数需要窗⼝中所有的值为⾮NA值。
    # 可以修改该⾏为以解决缺失数据的问题。其实，在时间序列开始处尚不⾜窗⼝期的那些数据就是个特例
    appl_std250 = close_px.AAPL.rolling(250, min_periods=10).std()
    printn('缺失值特例:', appl_std250[5:12])
    plt.figure()
    appl_std250.plot()
    # plt.show()

    # 要计算扩展窗⼝平均（expanding window mean），可以使⽤expanding⽽不是rolling。
    # “扩展”意味着，从时间序列的起始处开始窗⼝，增加窗⼝直到它超过所有的序列。
    expanding_mean = appl_std250.expanding().mean()

    # 对DataFrame调⽤rolling_mean（以及与之类似的函数）会将转换应⽤到所有的列上
    close_px.rolling(60).mean().plot(logy=True)
    # plt.show()

    # rolling函数也可以接受⼀个指定固定⼤⼩时间补偿字符串，⽽不是⼀组时期。
    # 这样可以⽅便处理不规律的时间序列。这些字符串也可以传递给resample。例如，我们可以计算20天的滚动均值，
    mean1 = close_px.rolling('20D').mean()
    printn('20天的滚动均值:', mean1.head())

    # 指数加权函数
    '''
    另⼀种使⽤固定⼤⼩窗⼝及相等权数观测值的办法是，定义⼀个衰减因⼦（decay factor）常量，以便使近期的观测值拥有更⼤的权数
    衰减因⼦的定义⽅式有很多，⽐较流⾏的是使⽤时间间隔（span），它可以使结果兼容于窗⼝⼤⼩等于时间间隔的简单移动窗⼝（simple moving window）函数。
    由于指数加权统计会赋予近期的观测值更⼤的权数，因此相对于等权统计，它能“适应”更快的变化。
    '''
    # 除了rolling和expanding，pandas还有ewm运算符。
    # 下⾯这个例⼦对⽐了苹果公司股价的30⽇移动平均和span=30的指数加权移动平均
    aapl_px = close_px.AAPL['2006':'2007']
    ma60 = aapl_px.rolling(30, min_periods=20).mean()
    ewma60 = aapl_px.ewm(span=30).mean()
    plt.figure()
    ma60.plot(style='k--', label='Simple MA')
    ewma60.plot(style='k-', label='EW MA')
    plt.legend()
    # plt.show()

    # 二元移动窗口函数
    '''
    有些统计运算（如相关系数和协⽅差）需要在两个时间序列上执⾏。
    例如，⾦融分析师常常对某只股票对某个参考指数（如标准普尔500指数）的相关系数感兴趣。
    要进⾏说明，我们先计算我们感兴趣的时间序列的百分数变化
    '''
    spx_px = close_px_all['SPX']
    spx_rets = spx_px.pct_change()
    returns = close_px.pct_change()
    # 调⽤rolling之后，corr聚合函数开始计算与spx_rets滚动相关系数
    corr = returns.AAPL.rolling(125, min_periods=100).corr(spx_rets)
    plt.figure()
    corr.plot()
    # plt.show()

    # 假设你想要⼀次性计算多只股票与标准普尔500指数的相关系数,只需传⼊⼀个TimeSeries和⼀个DataFrame，rolling_corr就会⾃动计算TimeSeries（本例中就是spx_rets）与DataFrame各列的相关系数
    corr = returns.rolling(125, min_periods=100).corr(spx_rets)
    corr.plot()
    # plt.show()

    # ⽤户定义的移动窗⼝函数
    # rolling_apply函数使你能够在移动窗⼝上应⽤⾃⼰设计的数组函数。
    # 唯⼀要求的就是：该函数要能从数组的各个⽚段中产⽣单个值（即约简）
    # ⽐如说，当我们⽤rolling(...).quantile(q)计算样本分位数时，可能对样本中特定值的百分等级感兴趣。
    # scipy.stats.percentileofscore函数就能达到这个⽬的
    from scipy.stats import percentileofscore
    score_at_2percent = lambda x: percentileofscore(x, 0.02)
    result = returns.AAPL.rolling(250).apply(score_at_2percent)
    plt.figure()
    result.plot()
    plt.show()


windowfunc()
